区块链第五版:实现钱包
源代码地址:https://github.com/daleboy/blockchain5
undefined前言
在上一篇中,我们把一个由用户定义的任意字符串当成是地址,现在我们将要实现一个跟比特币一样的真实地址。
undefined比特币地址
比特币地址是基于加密算法的组合创建的密钥,实际上是将公钥表示成人类可读的形式,并且保证这个世界上没有人其他人可以取走你的币,除非拿到你的私钥。
undefined公钥加密
公钥加密(public-key cryptography)算法使用的是成对的密钥:公钥和私钥。
本质上,比特币钱包也只不过是这样的密钥对而已。当你安装一个钱包应用,或是使用一个比特币客户端来生成一个新地址时,它就会为你生成一对密钥。在比特币中,谁拥有了私钥,谁就可以控制所以发送到这个公钥的币。
私钥和公钥只不过是随机的字节序列,因此它们无法在屏幕上打印,人类也无法通过肉眼去读取。这就是为什么比特币使用了一个转换算法(公钥加密),将公钥转化为一个人类可读的字符串(也就是我们看到的地址)。
公钥和私钥必须确保生成真正的随机字节。比特币采用椭圆曲线产生私钥。
比特币使用 Base58 算法将公钥转换成人类可读的形式(公钥加密算法)。这个算法跟著名的 Base64 很类似,区别在于它使用了更短的字母表:为了避免一些利用字母相似性的攻击,从字母表中移除了一些字母。也就是,没有这些符号:0(零),O(大写的 o),I(大写的i),l(小写的 L),因为这几个字母看着很像。另外,也没有 + 和 / 符号。
undefined每一笔交易输入都会由创建交易的人签名
比特币使用的是 ECDSA(Elliptic Curve Digital Signature Algorithm)算法来对交易进行签名,我们也会使用该算法。
在数学和密码学中,有一个数字签名(digital signature)的概念,算法可以保证:
- 当数据从发送方传送到接收方时,数据不会被修改;
- 数据由某一确定的发送方创建;
- 发送方无法否认发送过数据这一事实。
通过在数据上应用签名算法(也就是对数据进行签名),你就可以得到一个签名,这个签名晚些时候会被验证。生成数字签名需要一个私钥,而验证签名需要一个公钥。签名有点类似于印章,比方说我做了一幅画,完了用印章一盖,就说明了这幅画是我的作品。给数据生成签名,就是给数据盖了章。
为了对数据进行签名,我们需要下面两样东西:
- 要签名的数据
- 私钥
应用签名算法可以生成一个签名,并且这个签名会被存储在交易输入中。为了对一个签名进行验证,我们需要以下三样东西:
- 被签名的数据
- 签名
- 公钥
简单来说,验证过程可以被描述为:检查签名是由被签名数据加上私钥得来,并且公钥恰好是由该私钥生成。
数据签名并不是加密,你无法从一个签名重新构造出数据。这有点像哈希:你在数据上运行一个哈希算法,然后得到一个该数据的唯一表示。签名与哈希的区别在于密钥对:有了密钥对,才有签名验证。但是密钥对也可以被用于加密数据:私钥用于加密,公钥用于解密数据。不过比特币并不使用加密算法。
在比特币中,每一笔交易输入都会由创建交易的人签名。,创始区块例外,因为创始区块是没有输入的,所以也不需要签名。在被放入到一个块之前,必须要对每一笔交易进行验证。除了一些其他步骤,验证意味着:
- 检查交易输入有权使用来自之前交易的输出(同一人)
- 检查交易签名是正确的

undefined实现钱包地址
undefined钱包
钱包结构如下:
//Wallet 钱包结构type Wallet struct {PrivateKey ecdsa.PrivateKey//钱包私钥,谁拥有私钥,谁就拥有钱包PublicKey []byte}//Wallets 多个钱包type Wallets struct {Wallets map[string]*Wallet//value为struct类型,一般用map存储}//NewWallet 创建一个钱包func NewWallet() *Wallet {private, public := newKeyPair()wallet := Wallet{private, public}return &wallet}//newKeyPair 创建公私钥对func newKeyPair() (ecdsa.PrivateKey, []byte) {curve := elliptic.P256()private, err := ecdsa.GenerateKey(curve, rand.Reader)//生成私钥//公钥从私钥生成:将两个slice拼接到一起(这种方式使用append只能用两个参数,第二个参数的名称后需要加三个点)pubKey := append(private.PublicKey.X.Bytes(), private.PublicKey.Y.Bytes()...)return *private, pubKey}
undefined生成一个地址:将公钥转换为Base58地址
//GetAddress address=version(1个字节)+public key hash(32个字节)+checksum(4字节)func (w Wallet) GetAddress() []byte {pubKeyHash := HashPubKey(w.PublicKey)//version:生成算法的版本的前缀versionedPayload := append([]byte{version}, pubKeyHash...)checksum := checksum(versionedPayload)fullPayload := append(versionedPayload, checksum...)address := Base58Encode(fullPayload)return address}//HashPubKey 公钥哈希转为Base58编码func HashPubKey(pubKey []byte) []byte {publicSHA256 := sha256.Sum256(pubKey)RIPEMD160Hasher := ripemd160.New()_, err := RIPEMD160Hasher.Write(publicSHA256[:])publicRIPEMD160 := RIPEMD160Hasher.Sum(nil)return publicRIPEMD160}//校验值:4字节func checksum(payload []byte) []byte {firstSHA := sha256.Sum256(payload)secondSHA := sha256.Sum256(firstSHA[:])//两次哈希计算//addressChecksumLen=4,这里只截取secondSHA的4个字节长度切片return secondSHA[:addressChecksumLen]}
undefined修改输入和输出来使用地址
交易中输入者的身份,必须和输入中引用的输出者的身份相同(只有自己才有权处置自己的资产)
ype TXInput struct {Txid []byteVout int//引用的输出在原输出列表中的编号Signature []byte//输入的签名PubKey []byte//公钥}//UsesKey 是否可以解锁输入中的输出func (in *TXInput) UsesKey(pubKeyHash []byte) bool {lockingHash := HashPubKey(in.PubKey)return bytes.Compare(lockingHash, pubKeyHash) == 0}type TXOutput struct {Value intPubKeyHash []byte//公钥哈希,并不存储公钥本身}//Lock 设置输出中的公钥哈希,锁定输出func (out *TXOutput) Lock(address []byte) {pubKeyHash := Base58Decode(address)//解码出公钥pubKeyHash = pubKeyHash[1 : len(pubKeyHash)-4]//除掉version和checksum得到公钥哈希out.PubKeyHash = pubKeyHash}//IsLockedWithKey 检查输出的拥有者,不直接检查公钥是否相同,而是检查公钥的哈希是否相同func (out *TXOutput) IsLockedWithKey(pubKeyHash []byte) bool {return bytes.Compare(out.PubKeyHash, pubKeyHash) == 0}
undefined交易签名
(1)交易必须被交易发起者使用自己的私钥进行签名。因为这是比特币里面保证发送方不会花费属于其他人的币的唯一方式。
(2)签名必须是有效的。如果一个签名是无效的,那么这笔交易就会被认为是无效的,因此,这笔交易也就无法被加到区块链中。
(3)我们现在离实现交易签名还差一件事情:用于签名的数据。一笔交易的哪些部分需要签名?又或者说,要对完整的交易进行签名?选择签名的数据相当重要。因为用于签名的这个数据,必须要包含能够唯一识别数据的信息。比如,如果仅仅对输出值进行签名并没有什么意义,因为签名不会考虑发送方和接收方。
考虑到交易解锁的是之前的输出,然后重新分配里面的价值,并锁定新的输出,那么必须要签名以下数据:
- 存储在已解锁输出的公钥哈希。它识别了一笔交易的“发送方”。(from)
- 存储在新的锁定输出里面的公钥哈希。它识别了一笔交易的“接收方”。(to)
- 新的输出值。
在比特币中,锁定/解锁逻辑被存储在脚本中,它们被分别存储在输入和输出的
ScriptSig和ScriptPubKey字段。由于比特币允许这样不同类型的脚本,它对ScriptPubKey的整个内容进行了签名。
可以看到,我们不需要对存储在输入里面的公钥签名。因此,在比特币里, 所签名的并不是一个交易,而是一个去除部分内容的输入副本,输入里面存储了被引用输出的ScriptPubKey。
可以看到,我们不需要对存储在输入里面的公钥签名。因此,在比特币里, 所签名的并不是一个交易,而是一个去除部分内容的输入副本,输入里面存储了被引用输出的ScriptPubKey。
//Sign 对当前交易进行签名,需要把输入所引用的输出交易prevTXs作为参数进行处理func (tx *Transaction) Sign(privKey ecdsa.PrivateKey, prevTXs map[string]Transaction) {if tx.IsCoinbase() {//coinbase交易没有实际输入,所以没有无需签名return}txCopy := tx.TrimmedCopy()//将会被签署的是修剪后的当前交易的交易副本,而不是一个完整交易:for inID, vin := range txCopy.Vin {//迭代副本中的每一个输入prevTx := prevTXs[hex.EncodeToString(vin.Txid)]//在每个输入中,`Signature`被设置为`nil`(Signature仅仅是一个双重检验,所以没有必要放进来)txCopy.Vin[inID].Signature = nil//`pubKey`被设置为所引用输出的`PubKeyHashtxCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHashtxCopy.ID = txCopy.Hash()txCopy.Vin[inID].PubKey = nilr, s, err := ecdsa.Sign(rand.Reader, &privKey, txCopy.ID)//签名的是交易副本的ID(即交易副本的哈希)signature := append(r.Bytes(), s.Bytes()...)//一个 ECDSA 签名就是一对数字。连接切片,构建签名//**副本中每一个输入是被分开签名的**//尽管这对于我们的应用并不十分紧要,但是比特币允许交易包含引用了不同地址的输入tx.Vin[inID].Signature = signature}}//TrimmedCopy 获得修剪后的交易副本func (tx *Transaction) TrimmedCopy() Transaction {var inputs []TXInputvar outputs []TXOutputfor _, vin := range tx.Vin {//包含了所有的输入和输出,但是`TXInput.Signature`和`TXIput.PubKey`被设置为`nil`//在调用这个方法后,会用引用的前一个交易的输出的PubKeyHash,取代这里的PubKeyinputs = append(inputs, TXInput{vin.Txid, vin.Vout, nil, nil})}for _, vout := range tx.Vout {outputs = append(outputs, TXOutput{vout.Value, vout.PubKeyHash})}txCopy := Transaction{tx.ID, inputs, outputs}return txCopy}
undefined验证签名
func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool {if tx.IsCoinbase() {returntrue}txCopy := tx.TrimmedCopy()//同一笔交易的副本curve := elliptic.P256()//生成密钥对的椭圆曲线for inID, vin := range tx.Vin {//迭代每个输入//以下代码跟签名一样,因为在验证阶段,我们需要的是与签名相同的数据prevTx := prevTXs[hex.EncodeToString(vin.Txid)]txCopy.Vin[inID].Signature = niltxCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHashtxCopy.ID = txCopy.Hash()txCopy.Vin[inID].PubKey = nil//解包存储在`TXInput.Signature`和`TXInput.PubKey`中的值//一个签名就是一对长度相同的数字。r := big.Int{}s := big.Int{}sigLen := len(vin.Signature)r.SetBytes(vin.Signature[:(sigLen / 2)])s.SetBytes(vin.Signature[(sigLen / 2):])//一个公钥(输入提取的公钥)就是一对长度相同的坐标。x := big.Int{}y := big.Int{}keyLen := len(vin.PubKey)x.SetBytes(vin.PubKey[:(keyLen / 2)])y.SetBytes(vin.PubKey[(keyLen / 2):])//从输入提取的公钥创建一个rawPubKeyrawPubKey := ecdsa.PublicKey{curve, &x, &y}//使用公钥验证副本的签名,是否私钥签名档结果一致(&r和&s是私钥签名txCopy.ID的结果)if ecdsa.Verify(&rawPubKey, txCopy.ID, &r, &s) == false {return false}}return true}
undefined获得之前的交易
//FindTransaction 根据交易ID,获得交易func (bc *Blockchain) FindTransaction(ID []byte) (Transaction, error) {bci := bc.Iterator()for {block := bci.Next()for _, tx := range block.Transactions {if bytes.Compare(tx.ID, ID) == 0 {return *tx, nil}}if len(block.PrevBlockHash) == 0 {break}}return Transaction{}, errors.New("没有找到交易")}//SignTransaction 签名之前的交易func (bc *Blockchain) SignTransaction(tx *Transaction, privKey ecdsa.PrivateKey) {prevTXs := make(map[string]Transaction)for _, vin := range tx.Vin {prevTX, err := bc.FindTransaction(vin.Txid)//输入中保存了引用输出交易的ID,由此可以查到之前的交易prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX}tx.Sign(privKey, prevTXs)}//VerifyTransaction 验证之前交易的签名func (bc *Blockchain) VerifyTransaction(tx *Transaction) bool {prevTXs := make(map[string]Transaction)for _, vin := range tx.Vin {prevTX, err := bc.FindTransaction(vin.Txid)//输入中保存了引用输出交易的ID,由此可以查到之前的交易prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX}return tx.Verify(prevTXs)}
undefined对当前交易进行签名和验证交易签名
签名在NewUTXOTransaction中进行:
func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {...tx := Transaction{nil, inputs, outputs}//inputs将签名部分设为了niltx.ID = tx.Hash()bc.SignTransaction(&tx, wallet.PrivateKey)//通过钱包的私钥进行签名return &tx}
在一笔交易被放入一个块之前进行验证:
func (bc *Blockchain) MineBlock(transactions []*Transaction) {var lastHash []bytefor _, tx := range transactions {if bc.VerifyTransaction(tx) != true {log.Panic("ERROR: 非法交易。")}}...}
undefined运行测试
1、创建两个钱包地址
2、创建新的区块链
3、转账
4、转账后各账户余额查询
5、打印区块链
可以看到,转账交易有两个输出,一个是收款人获得的币,一个是给发送者的找零。
6、打印本地所有钱包地址
